package netty.io.demo.NIO.handle;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;

/**
 * nio 客户端 通信
 *
 * @author lihh
 */
public class NIOClientHandle implements Runnable {
  
  private volatile boolean started;
  private final InetSocketAddress inetSocketAddress;
  private final Selector selector;
  private final SocketChannel socketChannel;
  
  public NIOClientHandle(InetSocketAddress inetSocketAddress) throws IOException {
    this.inetSocketAddress = inetSocketAddress;
    
    // 1。 创建 选择器
    this.selector = Selector.open();
    // 2. 创建 监听通道
    this.socketChannel = SocketChannel.open();
    // 3. 从这里设置 非阻塞
    this.socketChannel.configureBlocking(false);
    
    this.started = true;
  }
  
  /**
   * 避免循环的方法
   *
   * @author lihh
   */
  public void stop() {
    this.started = false;
  }
  
  /**
   * 这里进行连接的创建
   *
   * @author lihh
   */
  private void doConnect() throws IOException {
    /*如果此通道处于非阻塞模式，则调用此方法将启动非阻塞连接操作。
        如果连接马上建立成功，则此方法返回true。
        否则，此方法返回false，
        因此我们必须关注连接就绪事件，
        并通过调用finishConnect方法完成连接操作。*/
    
    // 因为是非阻塞的，如果不一定会立马连接成功的，可能返回false
    if (this.socketChannel.connect(this.inetSocketAddress))
    // 注册 read 事件
    {
      this.socketChannel.register(this.selector, SelectionKey.OP_READ);
    } else {
      this.socketChannel.register(this.selector, SelectionKey.OP_CONNECT);
    }
  }
  
  @Override
  public void run() {
    try {
      doConnect();
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
    
    // 一直都是循环的状态，直到调用 stop 方法
    while (this.started) {
      try {
        // 这个方法 是阻塞的方法, 直到有相应事件的时候，才不会阻塞的
        this.selector.select();
        // 拿到相应的 key 比如 read write connect 信号过来的时候，都能拿到
        Set<SelectionKey> selectionKeys = this.selector.selectedKeys();
        
        Iterator<SelectionKey> it = selectionKeys.iterator();
        SelectionKey key = null;
        while (it.hasNext()) {
          key = it.next();
          /*我们必须首先将处理过的 SelectionKey 从选定的键集合中删除。
                    如果我们没有删除处理过的键，那么它仍然会在事件集合中以一个激活
                    的键出现，这会导致我们尝试再次处理它。*/
          it.remove();
          
          // 处理响应的key
          this.handleInput(key);
        }
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }
    
    if (this.selector != null) {
      try {
        this.selector.close();
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }
  }
  
  /**
   * 处理响应的 key
   *
   * @param key selection key
   * @author lihh
   */
  private void handleInput(SelectionKey key) throws IOException {
    if (!key.isValid()) {
      return;
    }
    
    // 拿到当前的 channel
    SocketChannel channel = (SocketChannel) key.channel();
    
    // 判断是否为 连接 处理
    if (key.isConnectable()) {
      if (channel.finishConnect())
      // 这里 注册 read 事件
      {
        channel.register(this.selector, SelectionKey.OP_READ);
      }
      
      // 这里是进行 读处理
    } else if (key.isReadable()) {
      // 创建 ByteBuffer 并且开辟1k 的缓冲区
      ByteBuffer buffer = ByteBuffer.allocate(1024);
      /*将通道的数据读取到缓冲区，read方法返回读取到的字节数*/
      int readBytes = channel.read(buffer);
      
      if (readBytes > 0) {
        buffer.flip();
        byte[] bytes = new byte[buffer.remaining()];
        buffer.get(bytes);
        String result = new String(bytes, StandardCharsets.UTF_8);
        System.out.println("客户端收到消息：" + result);
      } else if (readBytes < 0) {
        key.cancel();
        channel.close();
      }
    }
  }
  
  /**
   * 对外暴露的 发送消息的api
   *
   * @param message 消息
   * @author lihh
   */
  public void sendMessage(String message) throws IOException {
    doWrite(socketChannel, message);
  }
  
  /**
   * 通过 socket channel 写消息
   *
   * @param sc      socket channel
   * @param message 发送的消息
   * @author lihh
   */
  private void doWrite(SocketChannel sc, String message) throws IOException {
    byte[] bytes = message.getBytes();
    ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
    
    writeBuffer.put(bytes);
    writeBuffer.flip();
    sc.write(writeBuffer);
  }
}
